import sys
import random
import math
import os
import numpy as np
import pandas as pd
from operator import itemgetter

from collections import defaultdict

random.seed(0)


class ItemBasedCF(object):
    """
    集运itemcf的电影推荐
    """

    def __init__(self, path, itemcontentpath):
        self.n_sim_movie = 20  # 相似电影数目
        self.n_rec_movie = 10  # 推荐电影数目
        self.dataPath = path
        self.data = self.loadData()  # 加载数据集
        self.user_rating_matrix = self.generateUserRatingData(itemcontentpath)
        self.trainData, self.testData = self.splitData(0.7)  # 划分训练集和测试集
        # self.item_sim_mat=self.itemSimilarity()
        self.item_sim_mat = self.itemCosineSimilarity()

    def loadData(self):
        """
        打开数据文件，加载数据
        :return:
        """
        print("加载数据集...")
        data = []  # 定义数据数组
        # TODO：1.使用open方法加载文件
        fp = open(self.dataPath)
        # TODO：2.使用for循环遍历文件，获取每行信息
        for line in fp:
            userid, itemid, rating, _ = line.split("::")
            data.append((int(userid), int(itemid), int(rating)))
        return data

    def generateUserRatingData(self, itemPath):
        """
        生成用户评分矩阵
        :return:
        """
        print('开始处理数据...')
        itemContentMatrix = pd.read_csv(open(itemPath, encoding="unicode_escape"), engine="python", sep="::",
                                        header=None)

        # TODO：1.获取item个数（多少个电影）
        itemSize = itemContentMatrix[0].max()
        # TODO：2.获取用户个数（多少个用户）
        userSize = len(set([item[0] for item in self.data]))
        # TODO：3.构建用户对电影的评分矩阵
        rating_matrix = np.zeros((userSize + 1, itemSize + 1))
        for rating_item in self.data:
            rating_matrix[int(rating_item[0]), int(rating_item[1])] = int(rating_item[2])
        return rating_matrix

    def splitData(self, pivot=0.7):
        """
        划分训练集，测试集
        :param pivot:划分比例
        :return:
        """
        print("划分训练集和测试集...")
        train, test = {}, {}  # 定义字典

        # TODO:1.遍历循环每一行数据
        for userid, itemid, rating in self.data:

            # TODO:2.划分训练集和测试集，比例pivot
            # TODO:3.构建字典内容，主键key:userId,value:item-rating
            if random.random() < pivot:
                train.setdefault(userid, {})
                train[userid][itemid] = rating
            else:
                test.setdefault(userid, {})
                test[userid][itemid] = rating
        return train, test

    def itemSimilarity(self):
        """
        计算物品相似度矩阵
        :return:
        """
        # TODO: 1.计算每两个物品有多少用户共同发生过行为
        item_user_count = dict()  # 记录每个物品有多少用户发生过行为
        count = dict()  # 同现矩阵,记录两两物品有多少用户发生过行为

        for user, items in self.trainData.items():
            for i in items.keys():
                item_user_count.setdefault(i, 0)
                if self.trainData[user][i] > 0:
                    item_user_count[i] += 1
                    count.setdefault(i, defaultdict(int))
                for j in items.keys():
                    if self.trainData[user][i] > 0 and \
                            self.trainData[user][j] > 0 and i != j:
                        count[i][j] += 1

        # TODO: 2.计算相似度矩阵
        itemSim = dict()
        for i, related_movies in count.items():
            itemSim.setdefault(i, dict())
            for j, num in related_movies.items():
                itemSim[i][j] = num / math.sqrt(
                    item_user_count[i] * item_user_count[j])

        return itemSim

    def calCosineSimilarity(self, vec1, vec2):
        return np.dot(vec1, vec2.T) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

    def itemCosineSimilarity(self):
        """
        计算Item(电影)相似度
        :return:
        """
        print("计算Item与Item之间的相似度...")
        # TODO:  1.计算user相似用户
        itemSim = defaultdict(dict)
        user_rating_matrix_T = self.user_rating_matrix.T
        for i in range(1, len(user_rating_matrix_T) - 1):
            for j in range(i + 1, len(user_rating_matrix_T)):
                distance = self.calCosineSimilarity(user_rating_matrix_T[i], user_rating_matrix_T[j])
                itemSim[i][j] = distance
                itemSim[j][i] = distance
        return itemSim

    def recommend(self, user):
        """
         完成推荐电影
         找到K个相似电影和推荐N个电影
         :param user: 用户
         :return:返回最接近的N电影
         """
        K = self.n_sim_movie
        N = self.n_rec_movie
        rank = {}
        # TODO: 1.找到用户评分过的电影
        watched_movies = self.trainData.get(user, {})
        # TODO: 2.遍历看过的电影以及相应评分
        # TODO: 3.针对每个电影找出K个相似电影，并累加相似度=相似度*用户评分
        # TODO: 4.排序，找到最接近的N相似电影并返回
        for movie, rating in watched_movies.items():
            for similar_movie, similarity_factor in sorted(self.item_sim_mat[movie].items(),
                                                           key=itemgetter(1), reverse=True)[:K]:
                if similar_movie in watched_movies:
                    continue
                rank.setdefault(similar_movie, 0)
                rank[similar_movie] += similarity_factor * rating

        # print(sorted(rank.items(), key=itemgetter(1), reverse=True)[:N])
        return dict(sorted(rank.items(), key=itemgetter(1), reverse=True)[:N])

    def evaluate(self):
        """
        评价结果
        精确率
        :return:
        """
        print("开始计算...")
        N = self.n_rec_movie
        hit = 0
        rec_count = 0
        # TODO:1.遍历训练中的用户
        # TODO:2.获取测试集中相应的用户看的电影
        # TODO:3.调用recommend推荐算法获取推荐电影
        # TODO:4.计算相应的hit值，精确率和召回率
        for user in self.trainData.keys():
            test_items = self.testData.get(user, {})
            rec_items = self.recommend(user)
            for item, _ in rec_items.items():
                if item in test_items:
                    hit += 1
            rec_count += N
        # TODO:5.计算precision

        return hit / (1.0 * rec_count)


if __name__ == '__main__':
    """
    程序入口
    """
    ratingfile = os.path.join('data', 'ratings.dat')
    itemcontentpath = os.path.join('data', 'movies.dat')
    itemcf = ItemBasedCF(ratingfile, itemcontentpath)
    print("第1个item的相似其他item：", itemcf.item_sim_mat[1])
    print(itemcf.recommend(1))
    print(itemcf.evaluate())
